﻿// ////////////////////////////////////////////////////////////////////////////
// 
// SWE Case Tool
// 
// Solution:	CaseTool
// Project:		4. MethodCocomo
// Filename:	CocomoMethod.cs
// 
// File Created:	19:02 05.06.2013 
// Last Changed:	19:35 05.06.2013
// 
// ////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using SWE.CaseTool.Utils;

namespace SWE.CaseTool.Methods
{
	/// <summary>
	/// The COCOMO Method usese the weight of predefined factors and a guess about the project size to
	/// determine the estimate developement time.
	/// </summary>
	[MethodName("COCOMO Methode"),
	 MethodDescription("Berechnet den Aufwand eines Projektes mithilfe der COCOMO Methode." +
	                   " Es werden festgelegte Gewichtungen bewertet, aus denen der Projek" +
	                   "taufwand.")]
	public class CocomoMethod : IMethod
	{
		/// <summary>
		/// Cocomo Internal parameter for calculation the estimated developement time.
		/// </summary>
		private enum Parameter
		{
			C0,
			C1,
			D0,
			D1
		}

		/// <summary>
		/// TThe Internal parameter for the calculation differ with each project size. This two dimensional
		/// array holds those parameters.
		/// </summary>
		private readonly double[,] _sizes = new double[3,4];

		/// <summary>
		/// The unique method identifier.
		/// </summary>
		private const string NAME = "COCOMO Methode";

		/// <summary>
		/// The COCOMO FACTOR PATH points to the xml file containing the predefined cocomo factors, with  their
		/// default, min. and max. values.
		/// </summary>
		private const string COCOMO_FACTORS_PATH = @"ExternalFiles\cocomodefault.xml";

		/// <summary>
		/// The project globally data dictionary for saving method specific data.
		/// </summary>
		private readonly Dictionary<string, string> _data;

		/// <summary>
		/// Initializes a new instance of the <see cref="CocomoMethod" /> class and loads the cocomo method with
		/// default values. Looks for the default cocomo template and creates a new one, if the template does
		/// not exist.
		/// </summary>
		public CocomoMethod()
		{
			Factors = new List<CocomoFactor>();

			var dir = Path.GetDirectoryName(COCOMO_FACTORS_PATH);
			if (dir != null && !Directory.Exists(dir))
			{
				Directory.CreateDirectory(dir);
			}

			if (!File.Exists(COCOMO_FACTORS_PATH))
			{
				CreateDefault(COCOMO_FACTORS_PATH);
			}

			LoadSizes();
			LoadFactors();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CocomoMethod" /> class with the values saved in the
		/// project global dictionary or default values.
		/// </summary>
		/// <param name="data">The data.</param>
		/// <exception cref="System.ArgumentException">Unable to deserialize the cocomo method</exception>
		public CocomoMethod(Dictionary<string, string> data)
			: this()
		{
			_data = data;

			string serializedData;
			if (data.TryGetValue(NAME, out serializedData))
			{
				if (!String.IsNullOrWhiteSpace(serializedData))
				{
					try
					{
						var doc = new XmlDocument();
						doc.LoadXml(serializedData);

						LoadFromXml(doc.DocumentElement);
					}
					catch (Exception ex)
					{
						throw new ArgumentException("Unable to deserialize the cocomo method", ex);
					}
				}
			}
		}

		/// <summary>
		/// Loads the cocomo internal parameter needed for the calculation.
		/// </summary>
		private void LoadSizes()
		{
			var xml = new XmlDocument();
			xml.Load(COCOMO_FACTORS_PATH);

			var xmlElement = xml.DocumentElement.GetNode("sizes");
			foreach (XmlElement size in xmlElement.GetElementsByTagName("size"))
			{
				var t = size.GetAttribute("name");
				var category = Enum.Parse(typeof (ProjectSize), t, true);
				foreach (var para in Enum.GetValues(typeof (Parameter)))
				{
					var node = size.GetNode(para.ToString());
					_sizes[(int) category, (int) para] = double.Parse(node.GetAttribute("value"));
				}
			}
		}

		/// <summary>
		/// Loads the standard factors given in the cocomo standard template.
		/// </summary>
		private void LoadFactors()
		{
			var xml = new XmlDocument();
			xml.Load(COCOMO_FACTORS_PATH);

			if (xml.DocumentElement != null)
			{
				var categories = xml.DocumentElement.GetElementsByTagName("category");
				foreach (XmlElement category in categories)
				{
					var name = category.GetAttribute("name");
					foreach (XmlElement element in category.GetElementsByTagName("factor"))
					{
						Factors.Add(new CocomoFactor
							{
								Category = name,
								Name = element.GetAttribute("name"),
								Max = Double.Parse(element.GetAttribute("max")),
								Min = Double.Parse(element.GetAttribute("min")),
								Value = Double.Parse(element.GetAttribute("value"))
							});
					}
				}
			}
		}

		/// <summary>
		/// Creates the default cocomo template if the file does not exist.
		/// </summary>
		/// <param name="cocomoFactorsPath">The cocomo factors path.</param>
		private void CreateDefault(string cocomoFactorsPath)
		{
			using (var writer = new StreamWriter(cocomoFactorsPath, false))
			{
				writer.Write(
					@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<CocomoFactors>
  <sizes>
    <size name=""Organic"">
      <C0 value=""2,4""/>
      <C1 value=""1,05""/>
      <D0 value=""2,5""/>
      <D1 value=""0,38""/>
    </size>
    <size name=""SemiDetached"">
      <C0 value=""3,0""/>
      <C1 value=""1,12""/>
      <D0 value=""2,5""/>
      <D1 value=""0,35""/>
    </size>
    <size name=""Embedded"">
      <C0 value=""3,6""/>
      <C1 value=""1,20""/>
      <D0 value=""2,5""/>
      <D1 value=""0,32""/>
    </size>
  </sizes>
  <category name=""Produktattribute"">
    <factor name=""Zuverlaessigkeit"" value=""1,10"" min=""0,75"" max=""1,40""/>
    <factor name=""Datenbankgroesse"" value=""0,94"" min=""0,94"" max=""1,16""/>
    <factor name=""Produktkomplexitaet"" value=""0,90"" min=""0,70"" max=""1,65""/>
  </category>
  <category name=""Rechnerattribute"">
    <factor name=""Zyklenzeit"" value=""1,00"" min=""1,00"" max=""1,66""/>
    <factor name=""Hauptspeicher"" value=""1,00"" min=""1,00"" max=""1,56""/>
    <factor name=""Stabilitaet"" value=""1,10"" min=""0,87"" max=""1,30""/>
    <factor name=""UEEK-Zyklus"" value=""1,00"" min=""0,87"" max=""1,15""/>
  </category>
  <category name=""Faehigkeit des Personals"">
    <factor name=""Systemanalyst"" value=""1,00"" min=""0,71"" max=""1,46""/>
    <factor name=""Programmierer"" value=""0,80"" min=""0,70"" max=""1,42""/>
    <factor name=""Erfahrung im Anwendungsgebiet"" value=""0,92"" min=""0,82"" max=""1,29""/>
    <factor name=""Kenntnis der IDE"" value=""0,93"" min=""0,90"" max=""1,21""/>
    <factor name=""Erfahrung in Programmiersprache"" value=""0,96"" min=""0,95"" max=""1,14""/>
  </category>
  <category name=""Projektattribute"">
    <factor name=""Moderne Programmiermethoden"" value=""0,90"" min=""0,82"" max=""1,24""/>
    <factor name=""Benutzung von Werkzeugen"" value=""1,00"" min=""0,83"" max=""1,24""/>
    <factor name=""Termindruck"" value=""1,00"" min=""1,10"" max=""1,23""/>
  </category>  
</CocomoFactors>");
			}
		}

		/// <summary>
		/// Serializes this instance to a xml node.
		/// </summary>
		/// <param name="element">The xml element.</param>
		private void SaveToXml(XmlElement element)
		{
			element.AddNode("selectedsize").InnerText = Size.ToString();

			element.AddNode("kdsi").InnerText = KDSI.ToString(CultureInfo.InvariantCulture);
			var xmlElement = element.AddNode("sizes");
			for (var i = 0; i < 3; i++)
			{
				var size = xmlElement.AddNode("size");
				size.AddAttribute("name", ((ProjectSize) i).ToString());

				for (var j = 0; j < 4; j++)
				{
// ReSharper disable SpecifyACultureInStringConversionExplicitly
					// No InvariantCulture, because double deserialization is culture specific (E.G 1.2 and 1,3)
					size.AddNode(((Parameter) j).ToString()).InnerText = _sizes[i, j].ToString();
// ReSharper restore SpecifyACultureInStringConversionExplicitly
				}
			}

			var factors = element.AddNode("factors");

			var tmp = new Dictionary<string, List<CocomoFactor>>();
			Factors.ForEach(x =>
				{
					if (!tmp.ContainsKey(x.Category))
					{
						tmp.Add(x.Category, new List<CocomoFactor>());
					}

					tmp[x.Category].Add(x);
				});

			foreach (var category in tmp)
			{
				var categoryNode = factors.AddNode("category");
				categoryNode.AddAttribute("name", category.Key);

				foreach (var value in category.Value)
				{
					var node = categoryNode.AddNode("factor");
					node.AddAttribute("name", value.Name);
					node.AddAttribute("value", value.Value);
					node.AddAttribute("min", value.Min);
					node.AddAttribute("max", value.Max);
				}
			}
		}

		/// <summary>
		/// Dserializes a COCOMO Method from the given xml node and copies it to this instance,
		/// </summary>
		/// <param name="element">The xml element.</param>
		private void LoadFromXml(XmlElement element)
		{
			Size = (ProjectSize) Enum.Parse(typeof (ProjectSize), element.GetNode("selectedsize").InnerText);
			KDSI = double.Parse(element.GetNode("kdsi").InnerText);

			var elements = element.GetNode("sizes").GetElementsByTagName("size");
			foreach (XmlElement size in elements)
			{
				var t = size.GetAttribute("name");
				var category = Enum.Parse(typeof (ProjectSize), t);
				foreach (var para in Enum.GetValues(typeof (Parameter)))
				{
					var node = size.GetNode(para.ToString());
					_sizes[(int) category, (int) para] = double.Parse(node.InnerText);
				}
			}

			Factors.Clear();
			var categories = element.GetNode("factors").GetElementsByTagName("category");
			foreach (XmlElement category in categories)
			{
				var name = category.GetAttribute("name");
				foreach (XmlElement tempelement in category.GetElementsByTagName("factor"))
				{
					Factors.Add(new CocomoFactor
						{
							Category = name,
							Name = tempelement.GetAttribute("name"),
							Max = Double.Parse(tempelement.GetAttribute("max")),
							Min = Double.Parse(tempelement.GetAttribute("min")),
							Value = Double.Parse(tempelement.GetAttribute("value"))
						});
				}
			}
		}

		/// <summary>
		/// Calculates this estimated development time of the current project.
		/// </summary>
		/// <returns></returns>
		public double Calculate()
		{
			var c0 = _sizes[(int) Size, (int) Parameter.C0];
			var c1 = _sizes[(int) Size, (int) Parameter.C1];
			var d0 = _sizes[(int) Size, (int) Parameter.D0];
			var d1 = _sizes[(int) Size, (int) Parameter.D1];

			var ai = Factors.Aggregate<CocomoFactor, double>(1, (current, tempFactor) => current*tempFactor.Value);

			var pm = c0*Math.Pow(KDSI, c1)*ai;
			var dev = d0*Math.Pow(pm, d1)*ai;

			return dev;
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged
		/// resources.
		/// </summary>
		public void Dispose()
		{
			var document = Xml.CreateDocument("COCOMO");
			SaveToXml(document.DocumentElement);

			using (var stringWriter = new StringWriter())
			{
				using (var xmlWriter = new XmlTextWriter(stringWriter))
				{
					document.WriteTo(xmlWriter);
					xmlWriter.Flush();

					_data[NAME] = stringWriter.GetStringBuilder().ToString();
				}
			}
		}

		/// <summary>
		/// Copies this instance to the given one. Makes a hard copy resulting in two independant but equal
		/// instances.
		/// </summary>
		/// <param name="cocomoMethod">The cocomo method.</param>
		public void CopyTo(CocomoMethod cocomoMethod)
		{
			cocomoMethod.Factors.Clear();
			foreach (var cocomoFactor in Factors)
			{
				cocomoMethod.Factors.Add((CocomoFactor) cocomoFactor.Clone());
			}

			cocomoMethod.KDSI = KDSI;
			cocomoMethod.Size = Size;
		}

		/// <summary>
		/// Gets or sets the cocomo factors.
		/// </summary>
		/// <value>
		/// The factors.
		/// </value>
		public List<CocomoFactor> Factors { private set; get; }

		/// <summary>
		/// Gets or sets the KDSI (Kilo Delivered Source Instructions).
		/// </summary>
		/// <value>
		/// The KDSI.
		/// </value>
		public double KDSI { set; get; }

		/// <summary>
		/// The estimated projects size <see cref="ProjectSize" />
		/// </summary>
		public ProjectSize Size;
	}
}